// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel;
using CIP4.Common;
using CIP4.Item;

namespace CIP4.Simple
{
    delegate string GetConnectionValueDelegate(string valueName, string value);

    public delegate ProviderTypesEnum GetConnectionValuesDelegate(Dictionary<string, string> values);

    // Ading a new DataProvider type - this is what you have to do (assume your DataProvider provider is a Foo provider)
    // 1. Implement the IDataProvider interface (Get, Save, Purge etc)
    // 2. Make sure the namespace in which the DataProvider class is defined is called CIP4.DataProvider.Foo
    //    when CIP4 is looking for the provider it will use that name and the run time bind will fail if it is not spelt correctly
    // 3. The class that implements the IDataProvider interface must be called 'DataProvider' 
    // 4. Your provider executable must be in the #CIP4_HOME#\Binaries directory for CIPl to be able to find 
    //    it. Add your provider to the setup project (make sure it goes in the setup's Binaries folder, not 
    //    the application folder). Look at the post build event on any of the providers to see how the executable 
    //    is copied around at build time
    // 5. Add 'Foo' to the ProviderTypesEnum enumeration below
    // 6. Add a CIP4ConnectionAttribute subtype called CIP4FooConnectionAttribute that has a property for each of the
    //    properties required by a Foo DataProvider. The property values must be supplied by InternalGetValues 
    //    and set by InternalSetValues calls
    // 7. Add another case statement to internalGetActualConnection()
    // 8. Look for any unit tests that take a CIP4ConnectionAttribute as a parameter and make sure your provider passes the test
    //    see for example tests that use 
    //        public void SimpleCRUD(CIP4ConnectionAttribute connection)
    //    in CIP4\Tests\DataProvidersTest\CrudTest.cs


    public enum ProviderTypesEnum
    {
        None,
        File,
        XmlFile,
        Sql,
        MySql,
        Dictionary,
        Cassandra,
        Mosso,
        S3,
        MongoDB,
        MongoDBSlice,
        Migrator,
        MultiThread,
        MassTransit,
        Slice,
        GenericSlice,
        Template,
    }

    [Browsable(true), TypeConverter(typeof(GenericTypeConverter))]
    public abstract class CIP4ConnectionAttribute : Attribute
    {
        // Why is this so darned complicated?
        // 1.   Connection attributes can be supplied in a number of different ways
        // 1.1      A conection attribute can be provided by attaching one of the CIP4ConnectionAttribute subtypes
        //          to a class as a part of the declaration - not a great idea for things like hostnames
        //          and a very bad idea for things like passwords - still something you might want to do occasionally
        // 1.2      A conection attribute can be provided by constructing one of the CIP4ConnectionAttribute subtypes
        //          and supplying the properties at construction time with the attribute being given to a StrongWrapper
        //          type object when the StrongWrapper is constructed - this is not too bad - you might construct the 
        //          attribute from values supplied form a secure source at run time.
        // 1.3      The values for a connection attribute (including the type of the attribute) can be provided at 
        //          run time through a delegate attached to a StrongWrapper object. Again the assumtion is that these
        //          values will come from a secure source - the advantage is that the code doesn't commit to a 
        //          specific type of DataProvider.
        //      The code is expected to work in such a way that any values provided by 1.1 will be overridden by 
        //      values supplied by 1.2, 1.2 will be overridden by 1.3. If a value is not provided by 1.3, 1.2 (if
        //      provided) will be be used, if a value is not provided by 1.2, 1.1 (if provided) will be used. 
        //      This is where much of the complexity comes from.
        // 2.   Connection attributes can be nested. This is always true for cache connections (if provided), and
        //      can be true for other types of connections, for example the MigratorConnection. Nested connections 
        //      are supposed to have the same behavior as top level connections so far as overrides and defaults
        //      are concerned. You cannot provide a nested connection for 1.1 (CLR attribute syntax won't allow it).
        //      For 1.2 nested connections are simply assigments of nested constructed objects. For 1.3 nested
        //      connection properties are identified by a prefix on the name part of the name-value pair that must 
        //      match the name of the property used in 1.2 (i.e. the property to which the nested constructed connection 
        //      is assigned).

        #region Static features

        public static CIP4ConnectionAttribute GetAttribute(Type fromType)
        {
            object[] objects = fromType.GetCustomAttributes(typeof(CIP4ConnectionAttribute), true);
            if (objects.Length > 0)
                return (CIP4ConnectionAttribute)objects[0];
            return null;
        }

        public static ProviderTypesEnum ProviderTypesEnumFromString(string value)
        {
            foreach (FieldInfo field in typeof(ProviderTypesEnum).GetFields())
            {
                if (!field.IsLiteral || field.Name == "none")
                    continue;
                if (value.ToLower() == field.Name.ToLower())
                    return (ProviderTypesEnum)field.GetValue(new object());
            }
            return ProviderTypesEnum.None;
        }

        /// <summary>
        /// This expects a connection string consisting of name-value pairs, the name value pairs 
        /// are seperated by ";", the name is seperated from the value by "="
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        public static CIP4ConnectionAttribute GetConnectionFromString(string connectionString)
        {
            string[] parts = connectionString.Split(new[] { ';' });

            Dictionary<string, string> propertyValues = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);

            foreach (string part in parts)
            {
                string name = part.Substring(0, part.IndexOf("="));
                string value = part.Substring(name.Length + 1);
                propertyValues.Add(name, value);
            }
            CIP4ConnectionAttribute cacheConnection = GetInnerConnection(propertyValues, "cache.");
            return InternalGetActualConnection(cacheConnection, propertyValues, string.Empty);
        }

        public static CIP4ConnectionAttribute GetActualConnection(
            GetConnectionValuesDelegate getValues,
            CIP4ConnectionAttribute constructorConnection,
            CIP4ConnectionAttribute classConnection)
        {
            Dictionary<string, string> propertyValues = InternalGetValues(getValues);
            GetValuesFromConnection(constructorConnection, string.Empty, propertyValues);
            if (constructorConnection != null)
                GetValuesFromConnection(constructorConnection.Cache, "cache.", propertyValues);
            GetValuesFromConnection(classConnection, string.Empty, propertyValues);
            if (propertyValues == null)
                throw new ArgumentException("Canot build a connection attribute object as no property values have been supplied");
            CIP4ConnectionAttribute cacheConnection = GetInnerConnection(propertyValues, "cache.");
            return InternalGetActualConnection(cacheConnection, propertyValues, string.Empty);
        }

        private static CIP4ConnectionAttribute InternalGetActualConnection(
            CIP4ConnectionAttribute cacheConnection,
            Dictionary<string, string> propertyValues,
            string qualifier)
        {
            InternalGetValueDelegate getValue = 
                delegate(string valueName)
                {
                    if (propertyValues.ContainsKey(qualifier + valueName))
                        return propertyValues[qualifier + valueName];
                    return string.Empty;
                };
            switch (getValue("ProviderKind").ToLower())
            {
                case "sql":
                    return new CIP4SqlConnectionAttribute
                    {
                        Database = getValue("Database"),
                        Host = getValue("Host"),
                        UserId = getValue("UserId"),
                        Password = getValue("Password"),
                        ApplicationName = getValue("ApplicationName"),
                        Cache = cacheConnection
                    };
                case "mysql":
                    return new CIP4MySqlConnectionAttribute
                    {
                        Database = getValue("Database"),
                        Host = getValue("Host"),
                        UserId = getValue("UserId"),
                        Password = getValue("Password"),
                        Cache = cacheConnection
                    };
                case "file":
                    bool compressData;
                    bool.TryParse(getValue("CompressData"), out compressData);
                    return new CIP4FileConnectionAttribute
                    {
                        CompressData = compressData,
                        BaseDirectoryPath = getValue("BaseDirectoryPath"),
                        Cache = cacheConnection,
                        SerializerKind = 
                            getValue("SerializerKind") == "CIP4Json" ? SerializerKindEnum.CIP4Json :
                            SerializerKindEnum.CIP4Xml
                    };
                case "dictionary":
                    bool temporary;
                    bool.TryParse(getValue("Temporary"), out temporary);
                    return new CIP4DictionaryConnectionAttribute
                    {
                        BaseDirectoryPath = getValue("BaseDirectoryPath"),
                        Temporary = temporary,
                        Cache = cacheConnection
                        // actually the current dictionary provider implementation ignores the cache on account of there's nothing faster than a dictionary
                    };
                case "s3":
                    return new CIP4S3ConnectionAttribute
                    {
                        SecretKey = getValue("SecretKey"),
                        AccessKey = getValue("AccessKey"),
                        Cache = cacheConnection
                    };
                case "mosso":
                    return new CIP4MossoConnectionAttribute
                    {
                        UserName = getValue("UserName"),
                        AccessKey = getValue("AccessKey"),
                        Cache = cacheConnection
                    };
                case "xmlfile":
                    return new CIP4XmlFileConnectionAttribute
                    {
                        XmlFileName = getValue("XmlFileName"),
                        Cache = cacheConnection
                    };
                case "cassandra":
                    return new CIP4CassandraConnectionAttribute
                    {
                        Keyspace = getValue("Keyspace"),
                        ColumnFamily = getValue("ColumnFamily"),
                        Cache = cacheConnection
                    };
                case "mongodb":
                    {
                        int port;
                        int.TryParse(getValue("Port"), out port);
                        if (port == 0)
                            port = 27017;
                        return new CIP4MongoDBConnectionAttribute
                        {
                            Server = getValue("Server"),
                            Port = port,
                            Database = getValue("Database"),
                            Cache = cacheConnection
                        };
                    }
                case "mongodbslice":
                    {
                        int port;
                        int.TryParse(getValue("Port"), out port);
                        if (port == 0)
                            port = 27017;
                        return new CIP4MongoDBSliceConnectionAttribute
                        {
                            Basket = getValue("Basket"),
                            BasketIndex = getValue("BasketIndex"),
                            Server = getValue("Server"),
                            Port = port,
                            Database = getValue("Database"),
                            Cache = cacheConnection
                        };
                    }
                case "migrator":
                    {
                        return new CIP4MigratorConnectionAttribute
                        {
                            Target = InternalGetActualConnection(
                                null, propertyValues, "target."),
                            Source = InternalGetActualConnection(
                                null, propertyValues, "source."),
                                
                        };
                    }
                case "multithread":
                    {
                        int numberOfThreads;
                        int.TryParse(getValue("NumberOfThreads"), out numberOfThreads);
                        if (numberOfThreads == 0)
                            numberOfThreads = CIP4MultiThreadConnectionAttribute.DEFAULTNUMBEROFTHREADS;
                        return new CIP4MultiThreadConnectionAttribute
                        {
                            NumberOfThreads = numberOfThreads,
                            Target = InternalGetActualConnection(
                                null, propertyValues, "target."),

                        };
                    }
                case "masstransit":
                    {
                        int timeOut;
                        int.TryParse(getValue("TimeOutInMilliSecs"), out timeOut);
                        if (timeOut == 0)
                            timeOut = CIP4MassTransitConnectionAttribute.TIMEOUTINMILLISECS;
                        return new CIP4MassTransitConnectionAttribute
                        {
                            TimeOutInMilliSecs = timeOut,
                            BusId = getValue("BusId")
                        };
                    }
                case "template":
                    {
                        return new CIP4TemplateConnectionAttribute
                        {
                            Connection = getValue("Connection")
                        };
                    }

                default:
                    throw new ArgumentException(string.Format("{0} is not a supported provider type", getValue("ProviderKind")));
            }
        }

        private static CIP4ConnectionAttribute GetInnerConnection(Dictionary<string, string> propertyValues, string qualifier)
        {
            InternalGetValueDelegate getValue = 
                delegate(string valueName)
                {
                    if (propertyValues.ContainsKey(qualifier + valueName))
                        return propertyValues[qualifier + valueName];
                    return string.Empty;
                };
            if (string.IsNullOrEmpty(getValue("ProviderKind")))
                return null;
            return InternalGetActualConnection(null, propertyValues, qualifier);
        }

        private delegate string InternalGetValueDelegate(string valueName);

        private static Dictionary<string, string> InternalGetValues(GetConnectionValuesDelegate getValues)
        {
            Dictionary<string, string> propertyValues = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
            if (getValues == null)
                return propertyValues;
            propertyValues.Add("ProviderKind", getValues(propertyValues).ToString());
            return propertyValues;
        }

        private static void GetValuesFromConnection(CIP4ConnectionAttribute connection, string qualifier, Dictionary<string, string> values)
        {
            if (connection == null)
                return;
            foreach (PropertyInfo propInfo in connection.GetType().GetProperties())
            {
                if (propInfo.DeclaringType == typeof(CIP4ConnectionAttribute))
                    continue;
                if (propInfo.PropertyType == typeof(CIP4ConnectionAttribute))
                    GetValuesFromConnection(
                        (CIP4ConnectionAttribute)propInfo.GetValue(connection, new object[] { }),
                        qualifier + propInfo.Name + ".", values);
                else
                {
                    object value = propInfo.GetValue(connection, new object[] { });
                    if (value != null && !values.ContainsKey(qualifier + propInfo.Name))
                        values.Add(qualifier + propInfo.Name, value.ToString());
                }
            }
        }

        #endregion

        public abstract ProviderTypesEnum ProviderKind { get; }

        public virtual SerializerKindEnum SerializerKind 
        { 
            get { throw new ApplicationException("Should never be called"); } 
            set { throw new ApplicationException(string.Format("Cannot set SerializerKind for {0}", GetType().Name)); } 
        }

        public CIP4ConnectionAttribute Cache { get; set; }

        public string ToString(bool showSensitiveData)
        {
            string text = string.Empty;
            foreach (PropertyInfo prop in GetType().GetProperties())
                if (prop.CanWrite)
                {
                    object value = prop.GetValue(this, new object[] { });
                    bool showData = showSensitiveData || 
                        !(prop.Name == "Password" ||
                        (prop.Name == "AcessKey" && ProviderKind == ProviderTypesEnum.Mosso) ||
                        prop.Name == "SecretKey");
                    if (value != null && value.ToString() != string.Empty && showData)
                        text += text.Seperator(",") + prop.Name + "=" + FormatObjectValue(value);
                }
            return ProviderKind + "{" + text + "}";
        }

        public override string ToString()
        {
            return ToString(false);
        }
        private static string FormatObjectValue(object value)
        {
            if (value.GetType().IsValueType || value.GetType().Name == "String")
                return value.ToString();
            return "{" + value + "}";
        }

    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4SqlConnectionAttribute : CIP4ConnectionAttribute
    {
        public string Database { get; set;}
        public string Host { get; set;}
        public string UserId { get; set;}
        public string Password { get; set;}
        public string ApplicationName { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Sql; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4MySqlConnectionAttribute : CIP4ConnectionAttribute
    {
        public string Database { get; set;}
        public string Host { get; set;}
        public string UserId { get; set;}
        public string Password { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.MySql; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4MossoConnectionAttribute : CIP4ConnectionAttribute
    {
        public string UserName { get; set;}
        public string AccessKey { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Mosso; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4S3ConnectionAttribute : CIP4ConnectionAttribute
    {
        public string AccessKey { get; set;}
        public string SecretKey { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.S3; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4XmlFileConnectionAttribute : CIP4ConnectionAttribute
    {
        public string XmlFileName { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.XmlFile; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4FileConnectionAttribute : CIP4ConnectionAttribute
    {
        public string BaseDirectoryPath { get; set;}
        public bool CompressData { get; set; }
        private SerializerKindEnum _serializerKind = SerializerKindEnum.CIP4Xml;
        public override SerializerKindEnum SerializerKind { get { return _serializerKind; } set { _serializerKind = value; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.File; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4DictionaryConnectionAttribute : CIP4ConnectionAttribute
    {
        public string BaseDirectoryPath { get; set;}
        public bool Temporary { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Dictionary; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4CassandraConnectionAttribute : CIP4ConnectionAttribute
    {
        public string Keyspace { get; set;}
        public string ColumnFamily { get; set;}
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Cassandra; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4MongoDBConnectionAttribute : CIP4ConnectionAttribute
    {
        public string Server { get; set;}
        private int _portValue;
        public int Port
        {
            get
            {
                if (_portValue == 0)
                    return 27017;
                return _portValue;
            }
            set { _portValue =  value; }
        }
        public string Database { get; set;}
        /// <summary>
        /// Timeout in milliseconds - if not set will use the MongoDB default
        /// </summary>
        public int Timeout { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Json; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.MongoDB; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4MigratorConnectionAttribute : CIP4ConnectionAttribute
    {
        public CIP4ConnectionAttribute Target { get; set; }
        public CIP4ConnectionAttribute Source { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.Unknown; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Migrator; } }
    }

    public class CIP4MultiThreadConnectionAttribute : CIP4ConnectionAttribute
    {
        public const int DEFAULTNUMBEROFTHREADS = 2;
        private int _numberOfThreadsValue;
        public int NumberOfThreads
        {
            get
            {
                if (_numberOfThreadsValue == 0)
                    return DEFAULTNUMBEROFTHREADS;
                return _numberOfThreadsValue;
            }
            set { _numberOfThreadsValue = value; }
        }        
        public CIP4ConnectionAttribute Target { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.Unknown; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.MultiThread; } }
    }

    public class CIP4MassTransitConnectionAttribute : CIP4ConnectionAttribute
    {
        public const int TIMEOUTINMILLISECS = 5000;
        private int _timeOutInMilliSecs;
        public int TimeOutInMilliSecs
        {
            get
            {
                if (_timeOutInMilliSecs == 0)
                    return TIMEOUTINMILLISECS;
                return _timeOutInMilliSecs;
            }
            set { _timeOutInMilliSecs = value; }
        }
        public string BusId { get; set; }
        public CIP4ConnectionAttribute Target { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Xml; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.MassTransit; } }
    }

    public class CIP4SliceConnectionAttribute : CIP4ConnectionAttribute
    {
        // the parent property that contains the Child
        public string Basket { get; set; }
        // the Child property that contains the index into the basket property if the Basket property is MV
        public string BasketIndex { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.Unknown; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Slice; } }
    }

    public class CIP4GenericSliceConnectionAttribute : CIP4SliceConnectionAttribute
    {
        public CIP4ConnectionAttribute Container { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.Unknown; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.GenericSlice; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4MongoDBSliceConnectionAttribute : CIP4SliceConnectionAttribute
    {
        public string Server { get; set; }
        private int _portValue;
        public int Port
        {
            get
            {
                if (_portValue == 0)
                    return 27017;
                return _portValue;
            }
            set { _portValue = value; }
        }
        public string Database { get; set; }
        /// <summary>
        /// Timeout in milliseconds - if not set will use the MongoDB default
        /// </summary>
        public int Timeout { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.CIP4Json; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.MongoDBSlice; } }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class CIP4TemplateConnectionAttribute : CIP4ConnectionAttribute
    {
        public string Connection { get; set; }
        public override SerializerKindEnum SerializerKind { get { return SerializerKindEnum.Unknown; } }
        public override ProviderTypesEnum ProviderKind { get { return ProviderTypesEnum.Template; } }
    }

    /// <summary>
    /// This is used to persist connections available on the local machine
    /// </summary>
    [CIP4Item(AddAllProperties=true, Collection="CIPlConnections")]
    public class CIP4ConnectionItemType
    {
        static StrongWrapper<CIP4ConnectionItemType> _wrapper;
        public static StrongWrapper<CIP4ConnectionItemType> Wrapper
        {
            get
            {
                if (_wrapper == null)
                {
                    Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"CIP4"));
                    CIP4XmlFileConnectionAttribute connection = new CIP4XmlFileConnectionAttribute
                    {
                        XmlFileName = Globals.ConnectionsFileLocation
                    };
                    _wrapper = new StrongWrapper<CIP4ConnectionItemType>(connection, Globals.DefaultLogger);
                }
                return _wrapper;
            }
        }

        [CIP4ExternalID]
        public string Name { get; set; }

        [CIP4Property(SerializeTarget=true)]
        public CIP4ConnectionAttribute Connection { get; set; }
    }
}
